using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;

namespace InfiniTec.Data
{
	public class PropertyCollection : KeyedCollection<IPropertyDefinition, Property>
	{
		private readonly List<IPropertyDefinition> _RemovedProperties = new List<IPropertyDefinition>();

		/// <summary>
		/// Provides access to a list of removed properties.
		/// </summary>
		/// <value>The removed properties.</value>
		public IList<IPropertyDefinition> RemovedProperties
		{
			get { return _RemovedProperties; }
		}

		///<summary>
		///When implemented in a derived class, extracts the key from the specified element.
		///</summary>
		///
		///<returns>
		///The key for the specified element.
		///</returns>
		///
		///<param name="item">The element from which to extract the key.</param>
		protected override IPropertyDefinition GetKeyForItem(Property item)
		{
			return ((IPropertyInfo) item).PropertyDefinition;
		}


		///<summary>
		///Inserts an element into the <see cref="T:System.Collections.ObjectModel.KeyedCollection`2"></see> at the specified index.
		///</summary>
		///
		///<param name="item">The object to insert.</param>
		///<param name="index">The zero-based index at which item should be inserted.</param>
		///<exception cref="T:System.ArgumentOutOfRangeException">index is less than 0.-or-index is greater than <see cref="P:System.Collections.ObjectModel.Collection`1.Count"></see>.</exception>
		protected override void InsertItem(int index, Property item)
		{
			if (item == null) throw new ArgumentNullException("item");

			IPropertyDefinition definition = ((IPropertyInfo) item).PropertyDefinition;

			base.InsertItem(index, item);

			if (_RemovedProperties.Contains(definition))
			{
				_RemovedProperties.Remove(definition);
			}
		}

		protected override void ClearItems()
		{
			foreach (Property property in this)
			{
				_RemovedProperties.Add(((IPropertyInfo) property).PropertyDefinition);
			}
			base.ClearItems();
		}

		protected override void RemoveItem(int index)
		{
			_RemovedProperties.Add(((IPropertyInfo) this[index]).PropertyDefinition);
			base.RemoveItem(index);
		}


		///<summary>
		///Replaces the item at the specified index with the specified item.
		///</summary>
		///
		///<param name="item">The new item.</param>
		///<param name="index">The zero-based index of the item to be replaced.</param>
		protected override void SetItem(int index, Property item)
		{
			_RemovedProperties.Add(((IPropertyInfo) this[index]).PropertyDefinition);
			base.SetItem(index, item);

			IPropertyDefinition info = ((IPropertyInfo) item).PropertyDefinition;
			if (_RemovedProperties.Contains(info))
			{
				_RemovedProperties.Remove(info);
			}
		}

		public Property<T> GetProperty<T>(PropertyDefinition<T> definition)
		{
			Property property = ResolveProperty(definition);

			return (Property<T>) property;
		}

		private Property ResolveProperty<T>(PropertyDefinition<T> definition)
		{
			Property result;
			if (Contains(definition))
			{
				result = this[definition];
			}
			else
			{
				if (definition.IsMultivalued)
				{
					result = new MultiValuedProperty<T>(definition);
				}
				else
				{
					result = new Property<T>(definition);
				}
				Add(result);
			}

			var prototype = result as IPrototypeProperty;

			if (prototype != null)
			{
				Remove(result);
				if (definition.IsMultivalued)
				{
					result = prototype.ParseMultiValuedProperty<T>();
				}
				else
				{
					result = prototype.ParseProperty<T>();
				}
				Add(result);
			}

			//TODO: implement correct enum handling
			if (typeof (T).IsEnum && result is Property<int>)
			{
				result = new Property<T>(definition, (T) (object) ((Property<int>) result).Value);
				Dictionary[definition] = result;
			}
			return result;
		}

		public MultiValuedProperty<T> GetMultiValuedProperty<T>(PropertyDefinition<T> definition)
		{
			Property property = ResolveProperty(definition);

			return (MultiValuedProperty<T>) property;
		}

		public void Add<T>(PropertyDefinition<T> definition, T value)
		{
			Add(new Property<T>(definition, value));
		}

		public void Add<T>(PropertyDefinition<T> definition, IEnumerable<T> values)
		{
			Add(new MultiValuedProperty<T>(definition, values));
		}


		public bool TryGetProperty<T>(PropertyDefinition<T> definition, out Property<T> result)
		{
			if (!Contains(definition))
			{
				result = null;
				return false;
			}

			result = ResolveProperty(definition) as Property<T>;
			if (result == null) return false;

			if (result.Status != PropertyStatus.Ok)
			{
				result = null;
				return false;
			}
			return true;
		}

		public bool TryGetMultiValuedProperty<T>(PropertyDefinition<T> definition, out MultiValuedProperty<T> result)
		{
			if (!Contains(definition))
			{
				result = null;
				return false;
			}

			result = ResolveProperty(definition) as MultiValuedProperty<T>;
			if (result == null) return false;

			if (result.Status != PropertyStatus.Ok)
			{
				result = null;
				return false;
			}
			return true;
		}

		public T GetPropertyValue<T>(PropertyDefinition<T> definition)
		{
			return GetPropertyValue(definition, default(T));
		}

		public T GetPropertyValue<T>(PropertyDefinition<T> definition, T defaultValue)
		{
			Property<T> result;

			if (TryGetProperty(definition, out result))
			{
				return result.Value;
			}
			else
			{
				return defaultValue;
			}
		}

		public T? GetPropertyValue<T>(PropertyDefinition<T?> definition)
			where T : struct
		{
			return GetPropertyValue(definition, null);
		}

		public T? GetPropertyValue<T>(PropertyDefinition<T?> definition, T? defaultValue)
			where T : struct
		{
			Property<T?> result;

			if (TryGetProperty(definition, out result))
			{
				return result.Value;
			}
			// No direct match found. This may be because the collection contains the property
			// as Property<T> but is queried for Property<T?>
			foreach (Property property in this)
			{
				IPropertyInfo info = property;
				if (info.PropertyDefinition.Name != definition.Name) continue;

				return property.Status != PropertyStatus.Ok ? null : ConvertToValue<T>(info);
			}

			return defaultValue;
		}

		private static T? ConvertToValue<T>(IPropertyInfo info)
			where T : struct
		{
			Type targetType = typeof (T);
			Type sourceType = info.PropertyDefinition.Type;

			if (info.PropertyValueIsNull) return null;

			if (targetType.IsAssignableFrom(info.PropertyDefinition.Type)) return (T) info.Value;

			if (targetType.IsEnum && sourceType == Enum<T>.UnderlyingType)
			{
				return Enum<T>.ConvertFrom(info.Value);
			}

			var convertible = info.Value as IConvertible;
			if (convertible != null) return (T?) convertible.ToType(targetType, CultureInfo.InvariantCulture);

			return null;
		}

		public IList<T> GetMultiValuedPropertyValue<T>(PropertyDefinition<T> definition)
		{
			MultiValuedProperty<T> result;

			if (TryGetMultiValuedProperty(definition, out result))
			{
				return result.Values;
			}
			return new T[0];
		}

		public void SetPropertyValue<T>(PropertyDefinition<T> definition, T value)
		{
			Property<T> prop;

			if (ReferenceEquals(value, null))
			{
				Remove(definition);
				return;
			}

			if (TryGetProperty(definition, out prop))
			{
				prop.Value = value;
			}
			else
			{
				Property result = CreateProperty(definition, value);
				Add(result);
			}
		}

		private static Property CreateProperty<T>(PropertyDefinition<T> definition, T value)
		{
//            Type t = typeof(T);
//            Type propertyType;
//
//            propertyType = Nullable.GetUnderlyingType(t) ?? t;
//
//            t = typeof(Property<>);
//            t = t.MakeGenericType(propertyType);
			//   return (Property)Activator.CreateInstance(t, definition, Convert.ChangeType(value, propertyType));
			return new Property<T>(definition, value);
		}

		/// <summary>
		/// Ensures that the multivalued property with the specified name exists.
		/// </summary>
		/// <typeparam name="T">The type of the property</typeparam>
		/// <param name="definition">The property definition</param>
		public void EnsurePropertyExists<T>(PropertyDefinition<T> definition)
		{
			if (Contains(definition)) return;

			if (definition.IsMultivalued)
			{
				Add(new MultiValuedProperty<T>(definition));
			}
			else
			{
				Add(new Property<T>(definition));
			}
		}

		/// <summary>
		/// Copies the properties to the specified property collection.
		/// </summary>
		/// <param name="destination">The destination.</param>
		/// <param name="propertyNames">The property names.</param>
		/// <param name="markAsChanged"><see langword="true" />, if the properties should be marked as changed in the destination collection</param>
		public void CopyTo(PropertyCollection destination, bool markAsChanged, params IPropertyDefinition[] propertyNames)
		{
			Property property;

			if (destination == null)
			{
				throw new ArgumentNullException("destination");
			}

			if (propertyNames == null)
			{
				return;
			}

			foreach (IPropertyDefinition definition in propertyNames)
			{
				if (Contains(definition))
				{
					if (destination.Contains(definition))
					{
						destination.Remove(definition);
					}

					if (!Contains(definition) || this[definition].Status != PropertyStatus.Ok)
					{
						continue;
					}
					property = this[definition].Clone();
					if (markAsChanged)
					{
						property.SetChanged();
					}
					destination.Add(property);
				}

				if (definition.DependentProperties.Count > 0)
					CopyTo(destination, markAsChanged, new List<IPropertyDefinition>(definition.DependentProperties).ToArray());
			}
		}

		public void AddRange(IEnumerable<Property> properties)
		{
			if (properties == null) throw new ArgumentNullException("properties");

			foreach (Property property in properties)
			{
				if (Contains(property))
				{
					Remove(property);
				}
				Add(property);
			}
		}
	}
}